package com.uidp.security;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;

import javax.faces.context.FacesContext;
import javax.servlet.http.HttpSession;

import org.apache.log4j.Logger;

import com.uidp.domain.Users;


/**
 * Управлява данните в сесията за логнат потребител.
 * 
 */
public class SecurityUserManager {

	/**
	 * Потребител, който е текущо логнат в системата.
	 */
	private class LoggedUser {

		/** Максимален интервал на неактивност на потребителя. */
		private long inactiveTimeout;

		/** Последно време на активност на потребителя. */
		private long lastActivityTime;

		/** Уникален идентификатор за логнат потребител. */
		private UUID uniqueId;

		/** Потребител */
		private Users user;

		/**
		 * Инициализация на полетата, като последното време на достъп и
		 * максималния интервал на неактивност се инициализират със съответните
		 * на сесията, а уникалния идентификатор се генерира.
		 * 
		 * @param session
		 *            - потребителска сесия
		 * @param user
		 *            - глобален потребител
		 */
		private LoggedUser(HttpSession session, Users user) {
			this.lastActivityTime = session.getLastAccessedTime();
			this.inactiveTimeout = session.getMaxInactiveInterval() * 1000;
			this.uniqueId = UUID.randomUUID();
			this.user = user;
		}

		/**
		 * @return <code>true</code> ако разликата на текущото време и времето
		 *         на последен достъп е по-голяма от максималния интервал на
		 *         неактивност.
		 */
		boolean isObsolete() {
			return System.currentTimeMillis() - this.lastActivityTime > this.inactiveTimeout;
		}
	}

	/**
	 * Нишка която се грижи за изчистването от кеша на потребители с изтекла
	 * сесия.
	 */
	private class SessionCleaner extends Thread {

		/** Интервал на който нишката се активира. */
		private static final long SLEEP_INTERVAL = 10 * 1000L;

		/**
		 * На определен интервал се активира, като обикаля кеша с регистрираните
		 * потребители, и премахва тези, които са с изтекла сесия.
		 */
		@Override
		public void run() {
			while (true) {
				List<LoggedUser> loggedUsrs = SecurityUserManager.this.loggedUsers;
				for (int i = 0; i < SecurityUserManager.this.loggedUsers.size(); i++) {
					LoggedUser loggedUser = SecurityUserManager.this.loggedUsers.get(i);
					if (loggedUser.isObsolete()) {
						getLogger().debug(
								"user with entry id = " + loggedUser.uniqueId
										+ "is expired, removing...");
						loggedUsrs.remove(i--);
					}
				}
				try {
					sleep(SLEEP_INTERVAL);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}

	/** Единствената инстанция на класа. */
	private static SecurityUserManager instance;

	/**
	 * Singleton pattern.
	 * 
	 * @return единствената инстанция на класа
	 */
	public static SecurityUserManager getInstance() {
		if (instance == null) {
			instance = new SecurityUserManager();
		}
		return instance;
	}

	/** Списък с успешно логнати потребители. */
	private List<LoggedUser> loggedUsers;

	/** Логер */
	private Logger logger = null;

	/** Ключ под който стои информацията за глобалния потребител в сесията. */
	private final String SECURITY_TOKEN = "$Current_User$";

	/** Име на нишката за почистване на потребителите с изтекла сесия. */
	private final String SESS_CLEANER_NAME = "Session_Cleaner";

	/** Инстанция на нишката почистваща потребителите. */
	private Thread sessionCleaner;

	/**
	 * Сфтартиране на нишката за почистване и тази за синхронизация.
	 * Инициализация на кеша на потребителите.
	 */
	private SecurityUserManager() {
		this.logger = Logger.getLogger(getClass());
		this.loggedUsers = Collections.synchronizedList(new ArrayList<LoggedUser>());
		this.sessionCleaner = new SessionCleaner();
		this.sessionCleaner.setName(this.SESS_CLEANER_NAME);
		this.sessionCleaner.setDaemon(false);
		this.sessionCleaner.start();
	}

	/**
	 * Взема текущата сесия и ако тя е валидна търси в нея атрибута за
	 * автентикация на потребителя. Ако е наличен такъв, го извлича и върти
	 * цикъл по кеша с регистрираните потребители. При намерен елемент update-ва
	 * последното му време на достъп.
	 * 
	 * @return текущия потребител или <code>null</code>
	 */
	public Users getCurrentUser() {
		HttpSession session = getCurrentSession();
		if (session != null) {
			LoggedUser userEntry = (LoggedUser) session.getAttribute(this.SECURITY_TOKEN);
			if (userEntry != null) {
				if (!refreshSession(userEntry.uniqueId.toString())) {
					userEntry.user.setUsername(null);
					session.removeAttribute(this.SECURITY_TOKEN);
				}
				return userEntry.user;
			}
		}
		return null;
	}

	/**
	 * Проверява дали текущия потребител има достъп до дадена страница.
	 * 
	 * @param pageId
	 *            - път до страницата
	 * 
	 * @return страницата разрешена ли е или не
	 */
	public boolean hasAccessToPage(String pageId) {
		return getCurrentUser().getAllowedResources().contains(pageId);
	}

	/**
	 * Зарежда в сесията данни за текущия потребител
	 * 
	 * @param user
	 *            - потребител
	 * 
	 * @return успешен или не login
	 */
	public boolean login(Users user) {
		HttpSession session = getCurrentSession();
		if (session != null) {
			LoggedUser loggedUser = new LoggedUser(session, user);
			this.loggedUsers.add(loggedUser);
			session.setAttribute(this.SECURITY_TOKEN, loggedUser);
			return true;
		}
		return false;
	}

	/**
	 * Търси запис за логнат потребител в текущата сесия и ако намери такъв го
	 * премахва от сесията и от кеша с логнатите потребители. След това
	 * инвалидира сесията.
	 */
	public void logout() {
		HttpSession session = getCurrentSession();
		if (session != null) {
			LoggedUser user = (LoggedUser) session.getAttribute(this.SECURITY_TOKEN);
			if (user != null) {
				String uuid = user.uniqueId.toString();
				for (LoggedUser loggedUser : this.loggedUsers) {
					if (loggedUser.uniqueId.equals(uuid)) {
						loggedUser.lastActivityTime = 0;
						break;
					}
				}
				session.removeAttribute(this.SECURITY_TOKEN);
			}
			session.invalidate();
		}
	}

	/**
	 * Опреснява сесията на логнат потребител по даден уникален идентификатор.
	 * 
	 * @param uuid
	 *            - уникален идентификатор
	 * 
	 * @return <code>true</code> ако има такъв запис
	 */
	public boolean refreshSession(String uuid) {
		for (LoggedUser loggedUser : this.loggedUsers) {
			if (uuid.equals(loggedUser.uniqueId.toString())) {
				loggedUser.lastActivityTime = System.currentTimeMillis();
				return true;
			}
		}
		return false;
	}

	/**
	 * @return сесията от текущия faces context
	 */
	private HttpSession getCurrentSession() {
		FacesContext context = FacesContext.getCurrentInstance();
		return context != null ? (HttpSession) context.getExternalContext().getSession(true) : null;
	}

	/**
	 * @return logger
	 */
	private Logger getLogger() {
		return this.logger;
	}
}
